"""
====================================================================
Copyright (C) 2012-2013 Eric Blond

This file is part of Neptune.

Neptune is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

Neptune is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with Neptune.  If not, see <http://www.gnu.org/licenses/>.
====================================================================

Created on Jun 2, 2012

@author: Eric
"""

import collections
import weakref
import copy
from . import thread
from . import utils
from . import config
from . import exception

#######

def _hook():
    thread.GLOBALS.node_stack  = collections.deque()
    thread.GLOBALS.node_backup = collections.deque()
    thread.GLOBALS.node_notifs = None

thread.register(_hook)

#######

class BaseNode(object):
    
    __slots__ = ('__weakref__',
                 '_name',
                 '_owner',
                 '_type',
                 '_listener')

    def __init__(self, name     = None,
                       owner    = None,
                       type_    = None,
                       listener = None):
        self._name = name
        self._owner = owner
        self._type = type_
        if listener:
            nbargs = len(utils.arguments(listener))
            if nbargs == 2:
                self._listener = listener
            elif nbargs == 1:
                self._listener = lambda x, y: listener(y)
            elif nbargs == 0:
                self._listener = lambda x, y: listener()
        else:
            self._listener = None

    def __str__(self):
        if self._owner:
            if self._name:
                return '%s.%s' % (self._owner, self._name)
            else:
                return '%s.?' % self._owner
        else:
            if self._name:
                return '?.%s' % self._name
            else:
                return super(BaseNode, self).__str__()

    def _notify(self, old_value, new_value):
        if self._listener:
            try:
                self._listener(old_value, new_value)
            except:
                utils.LOGGER.info('Listener in %s raised an exception!', self, exc_info = True)

    def _check_thread(self):
        if hasattr(self._owner, '_NEPTUNE_thread_id'):
            if self._owner._NEPTUNE_thread_id != thread.GLOBALS.thread_id:
                raise exception.MultithreadException()

    def _check_value(self, value):
        if (value is not None) and self._type:
            self._type.check(value)
    
    def _check_within_get(self):
        if thread.GLOBALS.node_stack:
            raise exception.SetWithinGetException()

    def __enter__(self):
        value = copy.deepcopy(self.get())
        thread.GLOBALS.node_backup.append(value)
        return value
    
    def __exit__(self, typ, value, traceback):
        value = thread.GLOBALS.node_backup.pop()
        if typ is not None:
            raise typ, value, traceback
        else:
            self.set(value)

#######

class ConstantNode(BaseNode):
    
    __slots__ = ('_cache',)
    
    def __init__(self, value,
                       name     = None,
                       owner    = None,
                       type_    = None,
                       listener = None):
        super(ConstantNode, self).__init__(name     = name,
                                           owner    = owner,
                                           type_    = type_,
                                           listener = listener)
        self._check_value(value)
        self._cache = value

    def get(self):
        self._check_thread()
        return self._cache

#######

class StaticNode(ConstantNode):

    __slots__ = ('_parents',)
    
    def __init__(self, value,
                       name     = None,
                       owner    = None,
                       type_    = None,
                       listener = None):
        super(StaticNode, self).__init__(value    = value,
                                         name     = name,
                                         owner    = owner,
                                         type_    = type_,
                                         listener = listener)
        self._parents = weakref.WeakSet()

    def get(self):
        self._check_thread()
        stack = thread.GLOBALS.node_stack
        if stack:
            self._parents.add(stack[0])
        return super(StaticNode, self).get()

    def set(self, value):
        self._check_thread()
        self._check_within_get()
        if value != self._cache:
            self._check_value(value)
            old_value = self._cache
            self._cache = value
            candidates = []
            for parent in self._parents:
                parent._invalidate(candidates)
            self._parents.clear()
            notifs = thread.GLOBALS.node_notifs
            if notifs is None:
                notifs = collections.deque()
                thread.GLOBALS.node_notifs = notifs
                do_notifs = True
            else:
                do_notifs = False
            for candidate in candidates:
                candidate._recalculate()
            notifs.append((self, old_value, self._cache))
            if do_notifs:
                while notifs:
                    (node, old_value, new_value) = notifs.pop()
                    node._notify(old_value, new_value)
                thread.GLOBALS.node_notifs = None

#######

class DynamicNode(BaseNode):

    __slots__ = ('_cache',
                 '_parents',
                 '_function',
                 '_calculating')
    
    def __init__(self, function,
                       name     = None,
                       owner    = None,
                       type_    = None,
                       listener = None):
        super(DynamicNode, self).__init__(name     = name,
                                          owner    = owner,
                                          type_    = type_,
                                          listener = listener)
        self._cache = None
        self._parents = None
        self._function = function
        self._calculating = False
        if listener:
            self._recalculate()

    def _invalidate(self, candidates):
        if self._parents is not None:
            if self._listener:
                candidates.append(self)
            for parent in self._parents:
                parent._invalidate(candidates)
            self._parents = None

    def _recalculate(self):
        self._parents = weakref.WeakSet()
        stack = thread.GLOBALS.node_stack
        stack.appendleft(self)
        self._calculating = True
        old_value = self._cache
        try:
            self._cache = self._function()
            self._check_value(self._cache)
        except Exception:
            if config.NODE_LOG_CELL_EXCEPTIONS:
                utils.LOGGER.info('Body of %s raised exception!', self, exc_info = True)
            self._cache = None
        self._calculating = False
        stack.popleft()
        notifs = thread.GLOBALS.node_notifs
        if (notifs is not None) and (self._cache != old_value):
            notifs.append((self, old_value, self._cache))

    def get(self):
        self._check_thread()
        if self._calculating:
            raise exception.CircularDependencyError()
        if self._parents is None:
            self._recalculate()
        stack = thread.GLOBALS.node_stack
        if stack:
            self._parents.add(stack[0])
        return self._cache

#######

class WithSetDynamicNode(DynamicNode):
    
    __slots__ = '_on_set'
    
    def __init__(self, function,
                       on_set,
                       name     = None,
                       owner    = None,
                       type_    = None,
                       listener = None):
        super(WithSetDynamicNode, self).__init__(function = function,
                                                 name     = name,
                                                 owner    = owner,
                                                 type_    = type_,
                                                 listener = listener)
        self._on_set = on_set

    def set(self, value):
        self._check_thread()
        self._check_within_get()
        self._check_value(value)
        self._on_set(value)

#######

class SettableNode(DynamicNode):

    __slots__ = ('_override',
                 '_fallback')
    
    def __init__(self, function,
                       value    = None,
                       name     = None,
                       owner    = None,
                       type_    = None,
                       listener = None):
        self._override = StaticNode(value = value,
                                    name  = ('%s[override]' % name) if name else None,
                                    owner = self)
        self._fallback = function
        super(SettableNode, self).__init__(function = self._real_function,
                                           name     = name,
                                           owner    = owner,
                                           type_    = type_,
                                           listener = listener)

    def _real_function(self):
        value = self._override.get()
        if value is None:
            return self._fallback()
        else:
            return value[0]

    def is_set(self):
        return self._override.get() is not None

    def set(self, value):
        self._check_thread()
        self._check_within_get()
        self._check_value(value)
        self._override.set((value,))
    
    def reset(self):
        self._check_thread()
        self._check_within_get()
        self._override.set(None)
